package com.ld.shieldsb.es.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.commons.collections.CollectionUtils;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.BulkByScrollResponse;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestionBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;
import com.ld.shieldsb.common.core.model.PropertiesModel;
import com.ld.shieldsb.common.core.model.Result;
import com.ld.shieldsb.common.core.reflect.ModelUtil;
import com.ld.shieldsb.common.core.util.StringUtils;
import com.ld.shieldsb.dao.model.PageNavigationBean;
import com.ld.shieldsb.es.model.ElasticEntity;
import com.ld.shieldsb.es.model.ElasticSearchResult;
import com.ld.shieldsb.es.model.search.ElasticSearchQueryModel;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class BaseElasticService implements IElasticService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * 
     * 创建索引
     * 
     * @Title createIndex
     * @author 吕凯
     * @date 2020年8月7日 上午8:58:58
     * @param idxName
     *            索引名称
     * @param idxSQL
     *            索引描述,定义字段类型 void
     */
    /*{
        "dynamic":false, //动态字段映射规则。默认情况下，当在文档中发现以前未见过的字段时，Elasticsearch将向类型映射添加新字段。通过将映射参数dynamic 设置为false(忽略新字段)或strict(遇到未知字段时抛出异常)，可以在文档和对象级别禁用此行为。
        "properties":{
            "id":{"type":"long"},
            "flag":{
                "type":"text",
                "index":true
            },
            "localCode":{
                "type":"text",
                "index":true
            },
            "lv":{
                "type":"long"
            },
            "url":{
                "type":"text",
                "index":true
            }
        }
    }*/
    @Override
    public void createIndex(String idxName, String idxSQL) {
        try {
            if (!this.indexExist(idxName)) {
                log.warn(" idxName={} 已经存在,idxSql={}", idxName, idxSQL);
                return;
            }
            CreateIndexRequest request = new CreateIndexRequest(idxName);
            buildSetting(request);
            request.mapping(idxSQL, XContentType.JSON);
//            request.settings() 手工指定Setting
            CreateIndexResponse res = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
            if (!res.isAcknowledged()) {
                throw new RuntimeException("初始化失败");
            }
        } catch (Exception e) {
            log.error("", e);
            System.exit(0);
        }
    }

    /**
     * 设置分片
     * 
     * @Title buildSetting
     * @author 吕凯
     * @date 2020年8月7日 上午9:00:36
     * @param request
     *            void
     */
    private void buildSetting(CreateIndexRequest request) {
        request.settings(Settings.builder().put("index.number_of_shards", 3).put("index.number_of_replicas", 2));
    }

    /**
     * 删除索引
     * 
     * @Title deleteIndex
     * @author 吕凯
     * @date 2020年8月7日 上午11:47:45
     * @param idxName
     *            void
     */
    @Override
    public void deleteIndex(String idxName) {
        try {
            if (!this.indexExist(idxName)) {
                log.warn(" idxName={} 不存在", idxName);
                return;
            }
            restHighLevelClient.indices().delete(new DeleteIndexRequest(idxName), RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 
     * 断某个索引index是否存在
     * 
     * @Title indexExist
     * @author 吕凯
     * @date 2020年8月7日 上午8:59:33
     * @param idxName
     *            索引名
     * @return
     * @throws Exception
     *             boolean
     */

    public boolean indexExist(String idxName) throws Exception {
        GetIndexRequest request = new GetIndexRequest(idxName);
        request.local(false);
        request.humanReadable(true);
        request.includeDefaults(false);
        request.indicesOptions(IndicesOptions.lenientExpandOpen());
        return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
    }

    /**
     * 
     * 判断某个index是否存在
     * 
     * @Title isExistsIndex
     * @author 吕凯
     * @date 2020年8月7日 上午9:00:02
     * @param idxName
     *            索引名
     * @return
     * @throws Exception
     *             boolean
     */

    public boolean isExistsIndex(String idxName) throws Exception {
        return restHighLevelClient.indices().exists(new GetIndexRequest(idxName), RequestOptions.DEFAULT);
    }

    /**
     * 插入或更新对象,更新时需要传完整对象
     * 
     * @Title addOrUpdate
     * @author 吕凯
     * @date 2020年8月7日 上午9:00:57
     * @param idxName
     *            索引名
     * @param entity
     *            对象 void
     */
    @Override
    public Result addOrUpdate(String idxName, ElasticEntity entity) {
        Result result = new Result();
        IndexRequest request = new IndexRequest(idxName);
//            Map<?, ?> dataMap = ConvertUtil.obj2Map(entity.getData());
        String dataStr = JSON.toJSONString(entity.getData()); // 会忽略空字段
        log.debug("Data : id={},entity={}", entity.getId(), dataStr);
        request.id(entity.getId());
        request.source(dataStr, XContentType.JSON);
//        request.source(JSON.toJSONString(entity.getData()), XContentType.JSON);
        try {
            IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
            if (response.status() == RestStatus.OK) {
                result.setSuccess(true);
            }
        } catch (Exception e) {
            result.setMessage(e.getMessage());
            log.error("", e);
        }
        return result;
    }

    /**
     * 更新文档
     * 
     * @Title update
     * @author 吕凯
     * @date 2020年8月11日 上午9:03:46
     * @param idxName
     * @param entity
     * @throws IOException
     *             void
     */
    @Override
    public Result update(String idxName, ElasticEntity entity) {
        Result result = new Result();
        String dataStr = JSON.toJSONString(entity.getData()); // 会忽略空字段
        UpdateRequest request = new UpdateRequest(idxName, entity.getId());

        request.doc(dataStr, XContentType.JSON);
        try {
            UpdateResponse updateResponse = restHighLevelClient.update(request, RequestOptions.DEFAULT);
            if (updateResponse.status() == RestStatus.OK) {
                result.setSuccess(true);
            }
        } catch (Exception e) {
            result.setMessage(e.getMessage());
            log.error("", e);
        }

        return result;
    }

    /**
     * 
     * 批量插入数据
     * 
     * @Title insertBatch
     * @author 吕凯
     * @date 2020年8月7日 上午11:46:16
     * @param idxName
     *            索引名
     * @param list
     *            带插入列表 void
     */
    @Override
    public Result insertBatch(String idxName, List<ElasticEntity> list) {
        Result result = new Result();
        BulkRequest request = new BulkRequest();
        list.forEach(item -> request.add(new IndexRequest(idxName).id(item.getId()).source(item.getData(), XContentType.JSON)));
        try {
            BulkResponse response = restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
            if (response.status() == RestStatus.OK) {
                result.setSuccess(true);
            }
        } catch (Exception e) {
            result.setMessage(e.getMessage());
            log.error("", e);
        }
        return result;
    }

    /**
     * 
     * 批量删除
     * 
     * @Title deleteBatch
     * @author 吕凯
     * @date 2020年8月7日 上午11:46:45
     * @param <T>
     * @param idxName
     *            索引名
     * @param idList
     *            待删除列表 void
     */
    @Override
    public <T> Result deleteBatch(String idxName, Collection<T> idList) {
        Result result = new Result();
        BulkRequest request = new BulkRequest();
        idList.forEach(item -> request.add(new DeleteRequest(idxName, item.toString())));
        try {
            BulkResponse response = restHighLevelClient.bulk(request, RequestOptions.DEFAULT);
            if (response.status() == RestStatus.OK) {
                result.setSuccess(true);
            }
        } catch (Exception e) {
            result.setMessage(e.getMessage());
            log.error("", e);
        }
        return result;
    }

    /**
     * 
     * 搜索
     * 
     * @Title search
     * @author 吕凯
     * @date 2020年8月7日 上午11:47:12
     * @param <T>
     * @param idxName
     *            索引名
     * @param builder
     *            查询参数
     * @param clz
     *            结果类对象
     * @return List<T>
     */
    @Override
    public <T> ElasticSearchResult<T> search(String idxName, SearchSourceBuilder builder, Class<T> clazz) {
        ElasticSearchResult<T> pageBean = new ElasticSearchResult<T>();

        SearchRequest request = new SearchRequest(idxName);
        request.source(builder);
        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            pageBean.setTotalCount(response.getHits().getTotalHits().value); // 总页数
            List<T> res = new ArrayList<>(hits.length);

            String[] hlFieldNames = null; // 高亮字段
            if (builder.highlighter() != null) {
                int size = builder.highlighter().fields().size();
                hlFieldNames = new String[size];
                for (int i = 0; i < size; i++) {
                    hlFieldNames[i] = builder.highlighter().fields().get(i).name();
                }
            }

            for (SearchHit hit : response.getHits()) {
                Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                // =====定义高亮查询========
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if (hlFieldNames != null && hlFieldNames.length > 0) {
                    for (int i = 0; i < hlFieldNames.length; i++) {
                        String hlfieldName = hlFieldNames[i];
                        HighlightField nameHighlight = highlightFields.get(hlfieldName);

                        if (nameHighlight != null) {
                            Text[] fragments = nameHighlight.getFragments();
//                            String highlightField = "";
                            StringBuilder highlightFieldSb = new StringBuilder("");
                            for (Text text : fragments) {
//                                highlightField += text;
                                highlightFieldSb.append(text);
                            }
                            sourceAsMap.put(hlfieldName, highlightFieldSb.toString());
                        }
                    }
                }

                res.add(ModelUtil.map2model(sourceAsMap, clazz)); // ConvertUtil.map2obj(sourceAsMap, clazz)
            }
            pageBean.setResultList(res);
            return pageBean;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 分页查询
     * 
     * @Title getPageBean
     * @author 吕凯
     * @date 2020年8月13日 下午2:28:09
     * @param <T>
     * @param idxName
     *            索引名
     * @param classOfT
     *            类
     * @param queryModel
     *            搜索条件
     * @param highlightBuilder
     *            高亮处理
     * @param pageNum
     *            当前页码
     * @param pageSize
     *            每页条数
     * @param timeout
     *            超时
     * @return PageNavigationBean<T>
     */
    @Override
    public <T> PageNavigationBean<T> getPageBean(String idxName, Class<T> classOfT, ElasticSearchQueryModel queryModel,
            HighlightBuilder highlightBuilder, int pageNum, int pageSize, TimeValue timeout) {
        PageNavigationBean<T> pageBean = new PageNavigationBean<T>();
        pageBean.setCurrentPage(pageNum);
        pageBean.setPageSize(pageSize);

        // 构建搜索条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        QueryBuilder queryBuilder = QueryBuilders.matchAllQuery();

        if (queryModel != null) {
            queryBuilder = getQueryBuilder(queryModel);
            queryModel.getOrder(sourceBuilder);
            /*if (StringUtils.isNotEmpty(queryModel.getTitle())) {
                queryBuilder.must(QueryBuilders.multiMatchQuery(queryModel.getTitle(), "title", "content")); // 标题、内容
            }
            if (StringUtils.isNotEmpty(queryModel.getCreateName())) {
                queryBuilder.must(QueryBuilders.termQuery("createName.keyword", queryModel.getCreateName())); // 参考https://segmentfault.com/q/1010000017312707
            }
            if (StringUtils.isNotEmpty(queryModel.getCreateBy())) {
                queryBuilder.must(QueryBuilders.termQuery("createBy", queryModel.getCreateBy()));
            }
            if (StringUtils.isNotEmpty(queryModel.getIsBest())) {
                queryBuilder.must(QueryBuilders.termQuery("isBest", queryModel.getIsBest()));
            }
            if (StringUtils.isNotEmpty(queryModel.getType())) {
                queryBuilder.must(QueryBuilders.termQuery("type", queryModel.getType()));
            }*/
        }
        // 启动模糊查询
//        matchQueryBuilder.fuzziness(Fuzziness.AUTO);
        // 在匹配查询上设置前缀长度选项
//        matchQueryBuilder.prefixLength(3);
        // 设置最大扩展选项以控制查询的模糊过程
//        matchQueryBuilder.maxExpansions(10);
//        QueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("createName", "user").fuzziness(Fuzziness.AUTO).prefixLength(3)
//                .maxExpansions(10); //链式操作

        // QueryBuilders.wildcardQuery("createName", "*user*") // 模糊

        sourceBuilder.query(queryBuilder);
        if (timeout == null) { // 超时时间
            timeout = new TimeValue(15, TimeUnit.SECONDS);
        }
        sourceBuilder.timeout(timeout);

        int startRowNum = pageBean.getCurrentPoint() - 1;
        // 如下表示从第 startRowNum+1 条开始，共返回 pageSize 条文档数据
        sourceBuilder.from(startRowNum);
        sourceBuilder.size(pageSize);

        // 默认情况下，搜索请求会返回文档_source的内容，但与Rest API中的内容一样，您可以覆盖此行为。例如，您可以完全关闭_source检索：
        // sourceBuilder.fetchSource(false); // 不返回结果

        // 该方法还接受一个或多个通配符模式的数组，以控制以更精细的方式包含或排除哪些字段
//        String[] includeFields = new String[] { "title", "content", "i*" };
//        String[] excludeFields = new String[] { "_type" };
        String[] includeFields = null;
        String[] excludeFields = null;
        if (queryModel != null) {
            if (StringUtils.isNotEmpty(queryModel.getIncludeFields())) { // 返回
                includeFields = queryModel.getIncludeFields().split(",");
            }
            if (StringUtils.isNotEmpty(queryModel.getExcludeFields())) { // 不返回
                excludeFields = queryModel.getExcludeFields().split(",");
            }
        }
        sourceBuilder.fetchSource(includeFields, excludeFields);

        // =====定义高亮查询========
        if (highlightBuilder != null) {
            sourceBuilder.highlighter(highlightBuilder);
        }
        ElasticSearchResult<T> result = search(idxName, sourceBuilder, classOfT);

        pageBean.setTotalCount(result.getTotalCount().intValue());
        pageBean.setResultList(result.getResultList());
        return pageBean;
    }

    protected QueryBuilder getQueryBuilder(ElasticSearchQueryModel queryModel) {
        QueryBuilder queryBuilder = queryModel.getNoOrderQueryCondition();
        outLog(queryBuilder.toString());
        return queryBuilder;
    }

    /**
     * 输出日志
     * 
     * @Title outLog
     * @author 吕凯
     * @date 2020年9月17日 下午2:42:20
     * @param logMsg
     *            void
     */
    protected void outLog(String logMsg) {
        boolean outMain = PropertiesModel.CONFIG.getBoolean("log4j.out", false);
        boolean out = PropertiesModel.CONFIG.getBoolean("log4j.out.es", false);
        if (outMain && out) {
            log.warn(logMsg);
        }
    }

    /**
     * 
     * 删除文档
     * 
     * @Title deleteByQuery
     * @author 吕凯
     * @date 2020年8月7日 上午11:48:11
     * @param idxName
     * @param builder
     *            void
     */
    @Override
    public Result deleteByQuery(String idxName, QueryBuilder builder) {
        Result result = new Result();
        DeleteByQueryRequest request = new DeleteByQueryRequest(idxName);
        request.setQuery(builder);
        // 设置批量操作数量,最大为10000
        request.setBatchSize(10000);
        request.setConflicts("proceed");
        try {
            BulkByScrollResponse response = restHighLevelClient.deleteByQuery(request, RequestOptions.DEFAULT);
            if (response.getDeleted() == 1) {
                result.setSuccess(true);
            }
        } catch (Exception e) {
            result.setMessage(e.getMessage());
            log.error("", e);
        }
        return result;
    }

    /**
     * 根据id删除文档
     * 
     * @Title deleteById
     * @author 吕凯
     * @date 2020年8月11日 上午9:33:18
     * @param idxName
     * @param docId
     * @return Result
     */
    @Override
    public Result deleteById(String idxName, String docId) {
        Result result = new Result();
        DeleteRequest request = new DeleteRequest(idxName, docId);
        request.timeout(TimeValue.timeValueSeconds(3));
        try {
            DeleteResponse deleteResponse = restHighLevelClient.delete(request, RequestOptions.DEFAULT);
            if (deleteResponse.status() == RestStatus.OK) {
                result.setSuccess(true);
            }

            System.out.println(deleteResponse.status());
        } catch (Exception e) {
            result.setMessage(e.getMessage());
            log.error("", e);
        }
        return result;
    }

    /**
     * 自动补全，获取相关搜索、最多返回6条
     * 
     * @Title getSearchSuggest
     * @author 吕凯
     * @date 2020年8月19日 上午11:06:35
     * @param idxName
     * @param key
     * @param timeout
     * @return List<String>
     */

    public List<String> getSearchSuggest(String idxName, String columnName, String columnValue, TimeValue timeout,
            ElasticSearchQueryModel queryModel) {
        return getSearchSuggest(idxName, columnName, columnValue, 6, timeout, queryModel);
    }

    public List<String> getSearchSuggest(String idxName, String columnName, String columnValue, int size, TimeValue timeout,
            ElasticSearchQueryModel queryModel) {
        List<String> res = new ArrayList<>();
        if (StringUtils.isNotEmpty(columnName) && StringUtils.isNotEmpty(columnValue)) {
            int sizeSearch = size + 10;
            columnName = columnName.trim();
            columnValue = columnValue.trim();
            if (StringUtils.isNotEmpty(columnName) && StringUtils.isNotEmpty(columnValue)) {
                CompletionSuggestionBuilder suggestion = SuggestBuilders.completionSuggestion(columnName).prefix(columnValue)
                        .size(sizeSearch).skipDuplicates(true);
                SuggestBuilder suggestBuilder = new SuggestBuilder();
                suggestBuilder.addSuggestion("suggest", suggestion);

                // 构建搜索条件
                SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
                sourceBuilder.suggest(suggestBuilder);

                if (queryModel != null) {
                    BoolQueryBuilder queryBuilder = queryModel.getNoOrderQueryCondition();
                    queryModel.getOrder(sourceBuilder);
                    sourceBuilder.query(queryBuilder);
                }

                if (timeout == null) { // 超时时间
                    timeout = new TimeValue(15, TimeUnit.SECONDS);
                }
                sourceBuilder.timeout(timeout);

                // 默认情况下，搜索请求会返回文档_source的内容，但与Rest API中的内容一样，您可以覆盖此行为。例如，您可以完全关闭_source检索：
                sourceBuilder.fetchSource(false); // 不返回source结果

                SearchRequest request = new SearchRequest(idxName);
                request.source(sourceBuilder);
                try {
                    SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
                    Suggest suggest = response.getSuggest();

                    Set<String> keywords = null;
                    if (suggest != null) {
                        keywords = new HashSet<>();
                        List<? extends Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option>> entries = suggest
                                .getSuggestion("suggest").getEntries();

                        for (Suggest.Suggestion.Entry<? extends Suggest.Suggestion.Entry.Option> entry : entries) {
                            for (Suggest.Suggestion.Entry.Option option : entry.getOptions()) {
                                /** 最多返回10个推荐, 每个长度最大为20 */
                                String keyword = option.getText().string();
                                if (!StringUtils.isEmpty(keyword) /*&& keyword.length() <= 20*/) {
                                    /** 去除输入字段 */
                                    if (keyword.equals(columnValue)) {
                                        continue;
                                    }
                                    keywords.add(keyword);
                                    if (keywords.size() >= size) {
                                        break;
                                    }
                                }
                            }
                        }
                    }
                    if (CollectionUtils.isNotEmpty(keywords)) {
                        res.addAll(keywords);
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }

        }

        return res;
    }

}
